import jcogo.*;
import java.util.*;
import java.io.*;
import java.text.DecimalFormat;

/**
* This is the main program for jcogo. jcogo is a simple Java based
* coordinate geometry package.
* <p>
* This file is part of jcogo.
* <p>
* jcogo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* jcogo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with jcogo. If not, see <a href="http://www.gnu.org/licenses/">
* http://www.gnu.org/licenses/</a>
*
* @author Nathaniel Schwartz
*
* Copyright 2010 Nathaniel Schwartz.
*/

public class jcogo {
  public static void main(String[] args) {
    points = new ArrayList<Point>();
    decimalFormater = new DecimalFormat("#.####");
    
    // Use this Scanner throughout all main() and static methods as well.
    ioScanner = new Scanner(System.in);

    // Process command line arguments.
    // (Right now it only handles filenames)
    if (args.length > 0) {
      try {
        readFile(args[0]);
      }
      catch (Exception e) {
        System.out.println("File not found - invalid file name. ");
      }
    }

    // Clear the screen and show the main menu.
    printOptions();

    // Handle commands. These are self-explanatory.
    while (ioScanner.hasNext()) {
      String command = ioScanner.next();
      char c = command.toUpperCase().charAt(0);
      switch (c) {
        case 'Q': quit(); break;
        case 'X': quit(); break;
        case 'A':
          calculateArea();
          break;
        case 'B':
          setPointByBearing();
          break;
        case 'C':
          clearScreen();
          break;
        case 'D':
          editPoint();
          break;
        case 'E':
          try {
            setup();
          }
          catch (IllegalStateException e) {
            System.out.print(e.getMessage());
            break;
          }
          break;
        case 'H':
          showSetup();
          break;
        case 'I':
          inverse();
          break;
        case 'K':
          stakeout();
          break;
        case 'M':
          printOptions();
          break;
        case 'L':
          clearScreen();
          list();
          break;
        case 'O':
          open();
          break;
        case 'P':
          addPoint();
          break;
        case 'S':
          save();
          break;
        case 'T':
          traverse();
          break;
        default:
          System.out.print("\'" + command + "\' is not a valid option! ");
      }
    }
  }

  /**
   * Prints the current occupied and backsight points, or an error if
   * none exist.
   */
  private static void showSetup() {
    if (occupied == null || backsight == null) {
      System.out.print("There is no setup yet. ");
    }
    else {
      System.out.print(
      "Transit at " + occupied + "Backsight " + backsight
      + "Azimuth of " + backsight.getNumber() + " --> "
      + occupied.getNumber() + ": "
      + backsight.azimuthBetween(occupied));
    }
  }

  /**
   * Inverses between two given points. This is just calculating the
   * azimuth, distance and elevation change.
   */
  private static void inverse() {
    System.out.print("Enter \'#1 #2\': ");
    try {
      String command = getLine();
      Scanner iscan = new Scanner(command);
      int first = iscan.nextInt();
      int second = iscan.nextInt();
      if (!findPoint(first) || !findPoint(second)) {
        String error = "";
        if (!findPoint(first)) {
          error = "Point " + first + " does not exist. ";
        }
        else error = "Point " + second + " does not exist. ";
        throw new NoSuchElementException(error);
      }
      Point origin = getPoint(first);
      Point destination = getPoint(second);
      // Print out the information.
      System.out.println(origin.getNumber() + " --> "
        + destination.getNumber() + " = ["
        + origin.azimuthBetween(destination) + ", "
        + origin.distanceBetween(destination) + " HD, "
        + (origin.getZ() - destination.getZ()) + " VD] ");
    }
    catch (InputMismatchException e) {
      System.out.print("You entered \'"
        + ioScanner.next() + "\' which is not an integer. ");
      return;
    }
    catch (NoSuchElementException e) {
      System.out.print(e.getMessage());
      return;
    }
  }

  /**
   * Stakeout mode lets you enter a bunch of points by distance and
   * angle. Requires a valied setup.
   */
  private static void stakeout() {
    while (true) {
      try {
        // If one of the setup points is null, force a new setup.
        if (occupied == null || backsight == null) setup();
        System.out.print("(Q to quit) Angle right (DDD MM SS): ");
        String angle = getLine();
        // Check to see if the user wants to quit.
        char c = angle.charAt(0);
        if (c == 'q' || c == 'Q') {
          printOptions();
          return;
        }
        // This is the azimuth to the new point.
        Angle newAzimuth = 
          (Angle.add(new Angle(angle), 
          occupied.azimuthBetween(backsight)));
        System.out.print("Distance: ");
        double distance = ioScanner.nextDouble();
        System.out.print("Vertical change: ");
        double vchange = ioScanner.nextDouble();
        System.out.print("Point #: ");
        int number = ioScanner.nextInt();
        // Don't allow duplicate points. 
        boolean found = true;
        while (found) {
          if (findPoint(number)) {
            System.out.print("Point " + number + " exists! ");
            System.out.print("Enter a new number: ");
            number = ioScanner.nextInt();
          }
          else found = false;
        }
        System.out.print("Description: ");
        String description = getLine();
        // Truncate the description to 20 characters, if needed.
        if (description.length() > 20) {
          description = description.substring(0, 20);
        }
        // Calculate the new northing, easting and elevation values.
        double x = distance * Math.cos(newAzimuth.toRadians()) 
        	+ occupied.getX();
        double y = distance * Math.sin(newAzimuth.toRadians()) 
        	+ occupied.getY();
        double z = occupied.getZ() + vchange;
        Point newPoint = new Point(number, x, y, z, description);
        points.add(newPoint);
        list();
      }
      catch (InputMismatchException e) {
        System.out.println("Invalid angle entry - skipping. ");
        continue;
      }
      catch (IllegalStateException e) {
        System.out.print(e.getMessage());
        return;
      }
      catch (NullPointerException e) {
        System.out.print(e.getMessage());
        return;
      }
    }
  }

  /**
   * Add a new point by angle and distance, then move the occupied
   * point to the new point, backsight the old occupied point.
   * Requires a valid setup.
   */
  private static void traverse() {
    try {
      // If one of the setup points is null, force a new setup.
      if (occupied == null || backsight == null) setup();
      System.out.print("Angle right (DDD MM SS): ");
      Angle newAzimuth = 
          (Angle.add(new Angle(getLine()),
          occupied.azimuthBetween(backsight)));
      System.out.print("Distance: ");
      double distance = ioScanner.nextDouble();
      System.out.print("Vertical change: ");
      double vchange = ioScanner.nextDouble();
      System.out.print("Point #: ");
      int number = ioScanner.nextInt();
      // Don't allow duplicate points. 
      boolean found = true;
      while (found) {
        if (findPoint(number)) {
          System.out.print("Point " + number + " exists! ");
          System.out.print("Enter a new number: ");
          number = ioScanner.nextInt();
        }
        else found = false;
      }
      System.out.print("Description: ");
      String description = getLine();
      // Truncate the description to 20 characters, if needed.
      if (description.length() > 20) {
        description = description.substring(0, 20);
      }
      // Calculate the new northing, easting and elevation values.
      double x = distance * Math.cos(newAzimuth.toRadians()) 
      	+ occupied.getX();
      double y = distance * Math.sin(newAzimuth.toRadians()) 
      	+ occupied.getY();
      double z = occupied.getZ() + vchange;
      Point newPoint = new Point(number, x, y, z, description);
      points.add(newPoint);
      backsight = occupied;
      occupied = newPoint;
      // Remind user of the current setup.
      showSetup();
    }
    catch (IllegalStateException e) {
      System.out.print(e.getMessage());
      return;
    }
    catch (InputMismatchException e) {
      System.out.print(e.getMessage());
      return;
    }
  }

  /**
   * Stores an occupied and backsight state.
   */
  private static void setup() {
    try {
      System.out.print("Occupied: ");
      occupied = getPoint(ioScanner.nextInt());
      System.out.print("Backsight: ");
      backsight = getPoint(ioScanner.nextInt());
      if (occupied == backsight) {
        backsight = null;
        occupied = null;
        throw new IllegalStateException(
          "The backsight and occupied points must be distinct. ");
      }
    }
    catch (InputMismatchException e) {
      throw new IllegalStateException("Setup failed"
        + " because \'" + ioScanner.next()
        + "\' is not an integer. ");
    }
    catch (NullPointerException e) {
      throw new IllegalStateException("Setup failed"
        + " because: \'"
        + e.getMessage()+  "\'. ");
    }
  }

  /**
   * Add a point by entering coordinates.
   */
  private static void addPoint() {
    try {
      Point p = new Point();
      System.out.print("Enter #: ");
      int number = ioScanner.nextInt();
      // Don't allow duplicate points. 
      boolean found = true;
      while (found) {
        if (findPoint(number)) {
          System.out.print("Point " + number + " exists! ");
          System.out.print("Enter a new number: ");
          number = ioScanner.nextInt();
        }
        else found = false;
      }
      p.setNumber(number);
      System.out.print("Enter X: ");
      p.setX(ioScanner.nextDouble());
      System.out.print("Enter Y: ");
      p.setY(ioScanner.nextDouble());
      System.out.print("Enter Z: ");
      p.setZ(ioScanner.nextDouble());
      System.out.print("Enter description: ");
      String description = getLine();
      // Truncate the description to 20 characters, if needed.
      if (description.length() > 20) {
        description = description.substring(0, 20);
      }
      p.setDescription(description);
      points.add(p);
      list();
    }
    catch (InputMismatchException e) {
      System.out.print("Error. You entered \'"
        + ioScanner.next() + "\' which is not valid data. ");
    }
  }

  /**
   * Exit the program, close any open file writers while at it so that
   * the buffer gets written to the file.
   */
  private static void quit() {
    if (w != null) w.close();
    System.exit(0);
  }

  /**
   * Open a file, read the data, store the points, and list them when done.
   */
  private static void open() {
    try {
      System.out.print("Enter filename: ");
      readFile(ioScanner.next());
      list();
    }
    catch (FileNotFoundException e) {
      System.out.print("File not found - invalid filename. ");
      return;
    }
  }

  /**
   * Read the data from a coordinate file.
   * @param fileName the name of the coordinate file.
   */
  private static void readFile(String fileName) throws FileNotFoundException {
    FileReader reader = new FileReader(fileName);
    Scanner fileScanner = new Scanner(reader);
    // Keep track of line number to print meaningful error messages.
    int line = 1;
    while (fileScanner.hasNextLine()) {
      Scanner lineScanner = new Scanner(fileScanner.nextLine());
      if (lineScanner.hasNext()) {
        try {
          // Create a point. If the input is badly formatted, it will throw
          // an exception which will be caught below.
          Point p = new Point(lineScanner.nextInt(),
            lineScanner.nextDouble(), lineScanner.nextDouble(),
            lineScanner.nextDouble(), lineScanner.next());
          // Don't allow duplicate points.
          if (findPoint(p.getNumber())) {
            System.out.println("Point exists. Skipping. ");
            continue;
          }
          // Add to the points list.
          else points.add(p);
        }
        catch (NoSuchElementException e) {
          System.out.println("Line " + line 
            + " is badly formatted. Skipping ");
        }
      }
      line++;
    }
  }

  /**
   * Save the current points list to a file.
   */
  private static void save() {
    // Sort the points list for useability.
    Collections.sort(points);
    if (w == null) {
      boolean done = false;
      while (!done) {
        try {
          System.out.print("Enter a filename: ");
          w = new PrintWriter(ioScanner.next());
        }
        catch (FileNotFoundException e) {
          System.out.println("Invalid filename");
        }
        done = true;
      }
    }
    if (w == null) System.out.println("Printwriter null");
    for (Point p : points) {
      w.print(p);
    }
  }

  /**
   * List the points in the file, (print to screen).
   */
  private static void list() {
    if (points.size() == 0) {
      System.out.print("There are no points stored. ");
      return;
    }
    Collections.sort(points);
    for (Point p: points) {
      System.out.print(p);
    }
    System.out.print("\n\n\n");
  }

  /**
   * Edit a point's coordinates, number, description, etc.
   */
  private static void editPoint() {
    // Get a point to edit.
    Point p = new Point();
    try {
      clearScreen();
      System.out.print("Enter a point number: ");
      int number = ioScanner.nextInt();
      if (findPoint(number)) {
        p = getPoint(number);
      }
      else throw new IllegalStateException(
        "The point " + number + " does not exist. ");
    }
    catch (IllegalStateException e) {
      System.out.print(e.getMessage());
      return;
    }
    catch (NoSuchElementException e) {
      System.out.print(ioScanner.next() + " is not an integer. ");
      return;
    }
    catch (NullPointerException e) {
      System.out.print(e.getMessage());
      return;
    }

    // Take care of options.
    while (true) {
      showEditPointOptions();
      // Show the point each time we loop through.
      System.out.print("\n\t" + p);
      char c = ioScanner.next().toUpperCase().charAt(0);
      switch (c) {
        // Change the number.
        case 'N': 
          System.out.print("Enter number: ");
          if (ioScanner.hasNextInt()) {
            p.setNumber(ioScanner.nextInt());
            list();
          }
          else {
            System.out.print(ioScanner.next() + " is not an integer. ");
            break;
          }
          break;
        // Change the northing.
        case 'X':
          System.out.print("Enter northing: ");
          if (ioScanner.hasNextDouble()) {
            double northing = ioScanner.nextDouble();
            p.setX(northing);
            list();
          }
          else {
            System.out.print(ioScanner.next() + " is not a a valid number. ");
            break;
          }
          break;
        // Change the easting.
        case 'Y':
          System.out.print("Enter easting: ");
          if (ioScanner.hasNextDouble()) {
            double easting = ioScanner.nextDouble();
            p.setY(easting);
            list();
          }
          else {
            System.out.print(ioScanner.next() + " is not a a valid number. ");
            break;
          }
          break;
        // Change the elevation.
        case 'Z':
          System.out.print("Enter elevation: ");
          if (ioScanner.hasNextDouble()) {
            double elevation = ioScanner.nextDouble();
            p.setZ(elevation);
            list();
          }
          else {
            System.out.print(ioScanner.next() + " is not a a valid number. ");
            break;
          }
          break;
        // Change the description.
        case 'D':
          System.out.print("Enter a new description: ");
          String description = getLine();
          if (description.length() > 20) {
            description = description.substring(0, 20);
          }
          p.setDescription(description);
          break;
        // Delete the point.
        case 'R':
          System.out.print("Enter \'Y\' to confirm deletion of point " 
          	+ p.getNumber() + ". ");
          if (ioScanner.next().toUpperCase().charAt(0) == 'Y') {
            try {
              deletePoint(p.getNumber());
            }
            catch (NullPointerException e) {
              System.out.print(e.getMessage());
              break;
            }
            printOptions();
            return;
          }
          else {
            System.out.print("Aborted. ");
            break;
          }
        // Quit and show the main menu.
        case 'Q':
          printOptions();
          return;
        default:
          System.out.print("Invalid option. ");
      }
    }
  }

  /**
   * Find a point by it's number.
   * @param the point's number.
   * @return true if the point is in the list, false else.
   */
  private static boolean findPoint(int number) {
    // Sort points.
    Collections.sort(points);
    for (Point p: points) {
      if (p.getNumber() == number) {
        return true;
      }
      if (p.getNumber() > number) {
        return false;
      }
    }
    return false;
  }

  /**
   * Get a point from the Points list, given it's number.
   * @param number the Point number to get.
   * @return the Point requested.
   */
  private static Point getPoint(int number) {
    Point nullPoint = new Point();
    for (Point p: points) {
      if (p.getNumber() == number) {
        return p;
      }
    }
    // It's the caller's responsibility to check if the point is in the list
    // before asking for it. If it's not in the list we return a bogus point.
    return nullPoint;
  }

  /**
   * Deletes a Point given it's number.
   * @param number the number of the Point to delete.
   * @return the Point that was deleted.
   */
  private static Point deletePoint(int number) {
    for (int i = 0; i < points.size(); i++) {
      Point p = points.get(i);
      if (p.getNumber() == number) {
        points.remove(i);
        return p;
      }
    }
    throw new NullPointerException(
      "The point " + number + " doesn't exist. ");
  }

  /**
   * Calculate the area of a region. The region is defined by a sequence
   * of points which are given in clockwise or counterclockwise order
   * around a figure.
   */
  private static void calculateArea() {
    try {
      // A polygon must have at least 3 vertices to enclose area, so there
      // must be at least 3 in the list of points.
      if (points.size() < 3) {
        System.out.print("There are not enough points stored to "
          + "enclose any area. ");
        return;
      }
      System.out.print("Enter the point numbers separated by spaces. ");
      String numbers = ioScanner.nextLine();
      numbers = ioScanner.nextLine();
      Scanner ioScanner = new Scanner(numbers);
      // Add 2 so that the list is long enough to repeat the first and last
      // points. This is so the loop can access the i = n+1 and i = 0 cases.
      int i = 2;
      // Count up the number of entries in the sequence. Wasteful, but since
      // we don't do this operation much it's ok.
      while (ioScanner.hasNextInt()) {
        int x = ioScanner.nextInt();
        i++;
      }
      // If we didn't find any integers in the sequence, it's not valid.
      if (i == 2) {
        System.out.print("Invalid sequence. Aborting. ");
        return;
      }
      // If the user didn't enter enough points, we can't calculate area.
      if (i < 5) {
        System.out.print("At least three points are required. ");
        return;
      }
      ioScanner = new Scanner(numbers);
      Point[] sequence = new Point[i];
      // Use this to print out the sequence of points for the user to verify.
      String pointString = "";
      // Add points to the string above, add to list.
      for (int j = 1; j < i - 1; j++) {
        sequence[j] = getPoint(ioScanner.nextInt());
        pointString += "\t" + sequence[j];
      }
      // Add points so we can get the cases for the summation below.
      sequence[0] = sequence[i - 2];
      sequence[i - 1] = sequence[1];
      double sum = 0;
      // This is the area by coordinates part.
      for (int j = 1; j < i - 1; j++) {
        sum += sequence[j].getX() 
          * (sequence[j-1].getY() - sequence[j+1].getY());
      }
      double area = sum / 2.0;
      // Let it be noted that this could be more metric friendly.
      System.out.print("The area enclosed by the point sequence\n " 
        + pointString + "is "
        + Double.valueOf(decimalFormater.format(sum))
        + " square feet, or " 
        + Double.valueOf(decimalFormater.format(area / 43560.0))
        + " acres. ");
    }
    catch (NullPointerException e) {
      System.out.print(e.getMessage());
      return;
    }
  }

  /**
   * If you have a starting point and a bearing, you can set a new point
   * with that information. This is usually used for the first point in 
   * a survey when you don't have two points stored, but you have to have
   * a bearing to your backsight.
   */
  private static void setPointByBearing() {
    try {
      System.out.print("Enter the starting point: ");
      int startNumber = ioScanner.nextInt();
      Point p = getPoint(startNumber);
      System.out.print("Enter the azimuth (DDD MM SS): ");
      Angle azimuth = new Angle(getLine());
      System.out.print("Enter the distance: ");
      double distance = ioScanner.nextDouble();
      System.out.print("Enter the vertical change: ");
      double verticalChange = ioScanner.nextDouble();
      System.out.print("Enter the description: ");
      String description = getLine();
      if (description.length() > 20) {
        description = description.substring(0, 20);
      }
      System.out.print("Enter the new point: ");
      int number = ioScanner.nextInt();
      Point newPoint = new Point(
         number,
         distance * Math.cos(azimuth.toRadians()) + p.getX(),
         distance * Math.sin(azimuth.toRadians()) + p.getY(),
         verticalChange + p.getZ(),
         description);
      points.add(newPoint);
      System.out.println("Added point " + number + ". ");
      System.out.print(newPoint);
    }
    catch (NullPointerException e) {
      String x = ioScanner.next();
      System.out.print(e.getMessage());
      return;
    }
    catch (InputMismatchException e) {
      String x = ioScanner.next();
      System.out.print(x + " is invalid input. ");
      return;
    }
    catch (NoSuchElementException e) {
      String x = ioScanner.next();
      System.out.print("\'" + ioScanner.next() + "\' is invalid input. ");
      return;
    }
  }

  /**
   * Clears the screen on GNU/Linux, but doesn't do much on Windows.
   */
  private static void clearScreen() {
    System.out.println("\033[2J");
    System.out.flush();
  }

  /**
   * Changes the delimiter so that everything up to the newline
   * character is in the next token.
   * @return the String containing everything up to the newline
   *    character.
   */
  private static String getLine() {
    // Change the delimiter to the end of line character.
    ioScanner.useDelimiter(System.getProperty("line.separator"));
    String line = ioScanner.next();
    // Reset the Scanner to the usual delimiters.
    ioScanner.reset();
    return line;
  }

  /**
   * Print user options to the screen.
   */
  private static void printOptions() {
    clearScreen();
    System.out.print(
      "\tOptions:\n=============================================="
      + "\n\tA\tCalculate Area"
      + "\n\tB\tenter point by Bearing and distance"
      + "\n\tD\teDit Point"
      + "\n\tE\tsEtup"
      + "\n\tH\tsHow setup"
      + "\n\tI\tInverse"
      + "\n\tK\tstaKeout"
      + "\n\tL\tList points"
      + "\n\tM\tshow this Menu"
      + "\n\tO\tOpen a file"
      + "\n\tP\tenter a Point by coordinates"
      + "\n\tS\tSave current file"
      + "\n\tT\tTraverse"
      + "\n\tQ, X\tQuit or eXit the program "
      + "\n\n\n");
  }

  /**
   * Print user options for the edit point routine.
   */
  private static void showEditPointOptions() {
    clearScreen();
    System.out.println(
      "\t\tOptions:\n--------------------------------------------"
      + "\n\tChange point number - N " 
      + "\n\tChange northing - X "
      + "\n\tChange easting - Y "
      + "\n\tChange elevation - Z "
      + "\n\tChange description - D "
      + "\n\tDelete point - R "
      + "\n\tExit this routine - Q "
      + "\n\n\n"
    );
  }

  private static ArrayList<Point> points;
  private static Scanner ioScanner;
  private static File currentFile;
  private static PrintWriter w;
  private static Point occupied;
  private static Point backsight;
  private static DecimalFormat decimalFormater;
}
